﻿using System;
using System.IO;
using System.Net;
using mki.Backup.Helper;
using mki.Backup.Helper.Extensions;
using System.Configuration;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using mki.Backup.Objects;

namespace mki.Backup.Interfaces
{
	/// <summary>
	/// FTP Job
	/// </summary>
	internal class FTPJob
	{
		#region Fields (2)

		private static readonly Regex unixStyle = new Regex(@"^(?<dir>[-dl])(?<ownerSec>[-r][-w][-x])(?<groupSec>[-r][-w][-x])(?<everyoneSec>[-r][-w][-x])\s+(?:\d)\s+(?<owner>\w+)\s+(?<group>\w+)\s+(?<size>\d+)\s+(?<month>\w+)\s+(?<day>\d{1,2})\s+(?<year>\d{4}\s)?((?<hour>\d{1,2}):(?<minutes>\d{1,2})\s)?(?<name>.*)$", RegexOptions.Multiline | RegexOptions.Compiled);
		private static readonly Regex winStyle = new Regex(@"^(?<month>\d{1,2})-(?<day>\d{1,2})-(?<year>\d{1,2})\s+(?<hour>\d{1,2}):(?<minutes>\d{1,2})(?<ampm>am|pm)\s+(?<dir>[<]dir[>])?\s+(?<size>\d+)?\s+(?<name>.*)$", RegexOptions.Multiline | RegexOptions.Compiled);

		#endregion Fields

		#region Methods (6)

		// Public Methods (1) 

		/// <summary>
		/// Runs the job.
		/// </summary>
		/// <param name="job">The job.</param>
		/// <returns>
		/// True if job was executed successfully otherwise false if job failed
		/// </returns>
		public static Boolean RunJob(FTPJobElement job)
		{
			if (job.FTPHost.IsNullOrEmpty()) { throw new ArgumentException("FTPHost is null or empty!"); }
			if (job.RootFolder.IsNullOrEmpty()) { throw new ArgumentException("RootFolder is null or empty!"); }

			try
			{
				// add job
				var result = true;
				foreach (FTPAddElement file in job.AddFileCollection)
				{
					try
					{
						addFile(job, file);
					}
					catch (Exception ex)
					{
						Logging.WriteError("while adding file in ftp job '{0}': {1}\n{2}", job.Name, ex.Message, ex.StackTrace);
						result = false;
					}
				}

				// delete job
				foreach (FTPDeleteElement file in job.DeleteFileCollection)
				{
					try
					{
						if ((file.TargetFileName.IsNullOrEmpty() && file.OlderThanXDays <= 0) || (!file.TargetFileName.IsNullOrEmpty() && file.OlderThanXDays >= 0))
						{
							throw new ConfigurationErrorsException("Specifiy either TargetFileName or OlderThanXDays");
						}

						if (!file.TargetFileName.IsNullOrEmpty())
						{	// delete specific file
							deleteFile(job, file);
						}

						if (file.OlderThanXDays >= 0)
						{
							deleteFiles(job, file);
						}
					}
					catch (Exception ex)
					{
						Logging.WriteError("while deleting file in ftp job '{0}': {1}\n{2}", job.Name, ex.Message, ex.StackTrace);
						result = false;
					}
				}

				return result;
			}
			catch (Exception ex)
			{
				Logging.WriteError("while doing ftp job '{0}': {1}\n{2}", job.Name, ex.Message, ex.StackTrace);
				return false;
			}
		}
		// Private Methods (5) 

		/// <summary>
		/// Uploads the file.
		/// </summary>
		/// <param name="job">The job.</param>
		/// <param name="fileToAdd">The file to add.</param>
		private static void addFile(FTPJobElement job, FTPAddElement fileToAdd)
		{
			if (fileToAdd.SourceFileName.IsNullOrEmpty()) { throw new FileNotFoundException("Soruce file name is null or empty"); }
			var sourceFileName = fileToAdd.SourceFileName.ReplaceTokens();

			if (!File.Exists(sourceFileName)) { throw new FileNotFoundException(String.Format("Soruce File '{0}' could not be found!", sourceFileName)); }
			if (fileToAdd.TargetFileName.IsNullOrEmpty()) { throw new FileNotFoundException("Target file name is null or empty"); }
			var targetFileName = fileToAdd.TargetFileName.ReplaceTokens();

			var uri = getUri(job.FTPHost, job.RootFolder, targetFileName);

			Logging.WriteVerbose("Connecting to '{0}'", job.FTPHost);

			// create request
			var ftpWebRequest = WebRequest.Create(uri) as FtpWebRequest;
			if (ftpWebRequest == null) { return; }
			ftpWebRequest.Method = WebRequestMethods.Ftp.UploadFile;
			ftpWebRequest.KeepAlive = false;
			ftpWebRequest.UseBinary = true;
			ftpWebRequest.ReadWriteTimeout = 60 * 60 * 1000; // max one hour to progress

			// user and password?
			if (!String.IsNullOrWhiteSpace(job.UserName) && !String.IsNullOrWhiteSpace(job.Password)) { ftpWebRequest.Credentials = new NetworkCredential(job.UserName, job.Password); }

			// now copy the file
			using (var ftpStream = ftpWebRequest.GetRequestStream())
			{
				ftpStream.WriteTimeout = ftpWebRequest.ReadWriteTimeout;

				using (var fileStream = File.OpenRead(sourceFileName))
				{
					Logging.WriteVerbose("Uploading file '{0}' ({1})", sourceFileName, fileStream.Length.ToAutoSize());
					const Int32 length = 1024;
					var byteRead = 0;
					var buffer = new Byte[length];

					do
					{
						byteRead = fileStream.Read(buffer, 0, length);
						ftpStream.Write(buffer, 0, byteRead);
					} while (byteRead != 0);
				}
				ftpStream.Close();

				using (var response = ftpWebRequest.GetResponse() as FtpWebResponse)
				{
					if (response != null) { Logging.WriteVerbose("Upload File Complete, status {0}", response.StatusDescription.Trim()); }
				}
			}
			// delete file?
			if (fileToAdd.DeleteSourceAfterProcessing) { File.Delete(sourceFileName); }
		}

		/// <summary>
		/// Deletes the file.
		/// </summary>
		/// <param name="job">The job.</param>
		/// <param name="fileToDelete">The file to delete.</param>
		private static void deleteFile(FTPJobElement job, FTPDeleteElement fileToDelete)
		{
			var targetFileName = fileToDelete.TargetFileName.ReplaceTokens();

			var uri = getUri(job.FTPHost, job.RootFolder, targetFileName);

			Logging.WriteVerbose("Connecting to '{0}'", job.FTPHost);

			// create request
			var ftpWebRequest = WebRequest.Create(uri) as FtpWebRequest;
			if (ftpWebRequest == null) { return; }
			ftpWebRequest.Method = WebRequestMethods.Ftp.DeleteFile;

			// user and password?
			if (!String.IsNullOrWhiteSpace(job.UserName) && !String.IsNullOrWhiteSpace(job.Password)) { ftpWebRequest.Credentials = new NetworkCredential(job.UserName, job.Password); }

			// delete file
			Logging.WriteInformation("try to delete file '{0}'", targetFileName);
			using (var response = ftpWebRequest.GetResponse() as FtpWebResponse)
			{
				if (response != null) { Logging.WriteInformation("Delete status: {0}", response.StatusDescription.Trim()); }
			}
		}

		/// <summary>
		/// Deletes the files.
		/// </summary>
		/// <param name="job">The job.</param>
		/// <param name="fileToDelete">The file to delete.</param>
		private static void deleteFiles(FTPJobElement job, FTPDeleteElement fileToDelete)
		{
			Logging.WriteVerbose("Connecting to '{0}'", job.FTPHost);

			var uri = getUri(job.FTPHost, job.RootFolder);

			// create list directory request
			var ftpWebRequest = WebRequest.Create(uri) as FtpWebRequest;
			if (ftpWebRequest == null) { return; }
			ftpWebRequest.Method = WebRequestMethods.Ftp.ListDirectoryDetails;

			// user and password?
			if (!String.IsNullOrWhiteSpace(job.UserName) && !String.IsNullOrWhiteSpace(job.Password)) { ftpWebRequest.Credentials = new NetworkCredential(job.UserName, job.Password); }

			// get directory contents
			var files = new List<String>();
			using (var response = ftpWebRequest.GetResponse() as FtpWebResponse)
			{
				if (response == null) { return; }
				using (var responseStream = response.GetResponseStream())
				{
					using (var reader = new StreamReader(responseStream))
					{
						var line = reader.ReadLine();
						while (!line.IsNullOrEmpty())
						{
							files.Add(line);
							line = reader.ReadLine();
						}
					}
				}

				Logging.WriteInformation("Directory List Complete, status {0}", response.StatusDescription.Trim());
			}

			if (files.Count > 0)
			{
				foreach (var file in files)
				{
					try
					{
						var ftpMatch = parseFTPLine(file);

						// file or folder?
						if (ftpMatch.Groups["dir"].ToString() == "d") { continue; }

						// get filename and age
						var fileName = ftpMatch.Groups["name"].ToString();
						var fileCreationDate = DateTime.Parse(String.Format("{0}-{1}-{2} {3}:{4}", !ftpMatch.Groups["year"].Value.IsNullOrEmpty() ? ftpMatch.Groups["year"].Value : DateTime.Now.Year.ToString(), ftpMatch.Groups["month"], ftpMatch.Groups["day"], !ftpMatch.Groups["hour"].Value.IsNullOrEmpty() ? ftpMatch.Groups["hour"].Value : "00", !ftpMatch.Groups["minutes"].Value.IsNullOrEmpty() ? ftpMatch.Groups["minutes"].Value : "00"));

						if (!(fileCreationDate <= DateTime.Now.AddDays(fileToDelete.OlderThanXDays * -1))) { continue; }

						var canDelete = false;
						if (!fileToDelete.SearchPattern.IsNullOrEmpty())
						{	// check if file matches search pattern
							var searchPatternRegex = new Regex(String.Format("^{0}$", fileToDelete.SearchPattern.ReplaceTokens().Replace(".", @"\.").Replace(@"*", ".*").Replace(@"?", ".")));
							canDelete = searchPatternRegex.IsMatch(fileName);
						}
						else
						{	// seems to match delete conditions
							canDelete = true;
						}

						if (!canDelete) { continue; }

						// finally delete file
						try
						{
							deleteFile(job, new FTPDeleteElement { TargetFileName = fileName, Name = "customDeleteJob" });
						}
						catch (Exception ex)
						{
							Logging.WriteWarning("Could not delete file '{0}': {1}\n{2}", fileName, ex.Message, ex.StackTrace);
						}
					}
					catch (Exception ex)
					{
						Logging.WriteWarning("Could not parse line '{0}': {1}\n{2}", file, ex.Message, ex.StackTrace);
					}
				}
			}
		}

		/// <summary>
		/// Gets the URI.
		/// </summary>
		/// <param name="host">The host.</param>
		/// <param name="rootFolder">The root folder.</param>
		/// <param name="fileName">Name of the file.</param>
		/// <returns></returns>
		private static Uri getUri(String host, String rootFolder, String fileName = null)
		{
			if (fileName == null) { fileName = String.Empty; }
			var uri = new Uri(Path.Combine(host, rootFolder, fileName).Replace(@"\", "/"));

			//Logging.WriteVerbose("Uri: {0}", uri.ToString());

			if (uri.Scheme != Uri.UriSchemeFtp) { throw new UriFormatException(String.Format("Hostname '{0}' invalid", uri.ToString())); }

			return uri;
		}

		/// <summary>
		/// Parses the FTP line.
		/// </summary>
		/// <param name="line">The line.</param>
		/// <returns></returns>
		private static Match parseFTPLine(string line)
		{
			var match = unixStyle.Match(line);

			if (match.Success) { return match; }

			match = winStyle.Match(line);

			if (match.Success) { return match; }

			throw new Exception("Unkown Ftp.ListDirectoryDetails format");
		}

		#endregion Methods
	}
}